这里的“学习”是指从训练数据中自动获取最优权重参数的过程。

4.1 从数据中学习

4.1.1 数据驱动

如何从图片中识别“5”呢?

如图4.2所示。
机器学习领域,特征量仍是人工设计的,而深度学习中,特征量也是由机器来学习

tip:深度学习有时也被称为端到端机器学习(end-to-end machine learning)。这里的“端到端”指的是从原始数据(输入)到目标结果(输出)。而中间过程究竟使用了怎样的权重、怎样的神经网络、外界是无从得知的。

神经网络的优点是对所有的问题都可以用同样的流程来解决。无论是识别“5”、还是识别狗、识别人脸,均使用同样的模式。

4.1.2 训练数据和测试数据

机器学习中,一般将数据分为训练数据和测试数据两部分。
先利用训练数据来训练出模型、再用测试数据来评价训练得来的模型的实际能力。 训练数据也被称为“监督数据”

泛化能力: 处理未被观察过的数据(即训练集之外数据)的能力。 获得泛化能力是机器学习的最终目标

过拟合(over fitting): 没有泛化能力,仅仅对某个数据集过度拟合的状态

4.2 损失函数

损失函数(loss function): 用于评价神经网络性能的指标。

4.2.1 均方误差

均方误差(mean squared error)

这里, 表示神经网络的输出,表示监督数据,k表示数据的维数。
比如:在3.6节手写数字识别的例子中, 是如下元素组成的数据

  • y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
  • t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

y表示各种数字的概率
t表示正确答案。这种将正确解标签表示为1,其他标签表示为0的方法称为one-hot表示

python实现:

import numpy as np
def mean_squared_error(y, t):
    return 0.5 * np.sum((y - t)**2)
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
# 例:"2"的概率最高的情况
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
 # 例:“7”的概率最高的情况
print(mean_squared_error(np.array(y), np.array(t)))
# 输出为0.09750000000000003

y = [0.1, 0.05, 0.2, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print(mean_squared_error(np.array(y), np.array(t)))
# 输出为0.5125000000000001

显然, 第二组数据误差很大

4.2.2 交叉熵误差

交叉熵误差(cross entropy)也经常被用作损失函数

式中:ln为自然对数,为神经网络的输出, 中, 正确解的索引为1,其他均为0(one-hot表示)
交叉熵误差的值是由正确解标签的输出结果所决定的。
正确解标签的输出越大,则误差值越小
极限情况:若正确解的输出为0,则误差值为正无穷

代码实现:

import numpy as np
def cross_entropy_error(y, t):
    delta = 1e-7
    return -np.sum(t * np.log(y + delta))

这里为什么定义一个delta呢?
因为对数函数的自变量不可以是0,否则程序会终止。

t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0] #正确解对应的输出为0.6
print(cross_entropy_error(np.array(y), np.array(t)))
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0] #正确解对应的输出为0.1
print(cross_entropy_error(np.array(y), np.array(t)))

输出结果为:

0.510825457099338 
2.302584092994546

4.2.3 mini_batch学习

前面介绍的损失函数的例子仅仅是针对单个数据,但机器学习时需要计算大量数据的损失函数。
所以计算N条数据交叉熵函数可以写成:

公式解释:

  • 数据一共有N条
  • 表示第n个数据的第k个元素的标签
  • 表示第n和数据的第k个元素的输出

mini-batch学习:神经网络的训练中,数据可能会达到几百万、几千万之多,这种情况下对所以数据求损失函数并不现实。我们取其中的一小部分数据(称为mini-batch),然后对每个mini-batch进行学习。
这种学习方式称为mini-batch学习。

简而言之mini-batch就是取一部分样本来代表整体。

前面提到过MNIST数据集:

import sys, os
sys.path.append(os.pardir)
from dataset.mnist import load_mnist
(x_train, t_train),(x_test, t_test) = load_mnist(flatten=True,normalize=True, one_hot_label = True) # 线性化 规格化 one-hot表示
print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000, 10)
print(x_test.shape) # (10000, 784)
print(t_test.shape) # (10000, 10)

MINIST数据集中,共60000条训练数据, 每条数据为784(28×28)维,监督数据是10维。

如何从中抽取10组数据?? 我们使用了random.choice()方法

array = np.random.choice(60000,10) # 从0到60000中随机取10个数字
print(array)
 # 输出结果:[11913 29114 14056  6304 11747 12412 43446 59074 46544 17836]

4.2.4 mini-batch版交叉熵误差

一个可以同时处理单个数据和批量数据的函数:

def cross_entropy_error(y, t):
    if y.ndim == 1:  # 这一步为什么这样做俺也不明白,感觉不需要reshape啊
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
    batch_size = y.shape[0]
    # 如果数据是one-hot表示:
    return -np.sum(t * np.log(y + 1e-7)) / batch_size
    # 如果数据是非one-hot表示:
    # return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

4.2.5 为何要设定损失函数?

以前面的手写数字识别任务为例。
问题:既然该任务的目的是尽可能得提高识别精度,那么直接使用识别精度作为指标不就可以了吗?为什么还要使用损失函数?

首先解释一下“导数”在神经网络中的作用。在神经网络的学习中,我们需要利用“导数”来寻找最优算法。

  • 如果导数为负值,则需要设法改变参数使该点损失函数的值增大
  • 如果导数为负值,则需要设法改变参数使该点损失函数的值减小

假如以识别精度作指标,大部分点的导数值均为0.
比如100张图片,成功识别了32张,识别精度为32%。现在调整权重后,识别精度仍为32%,那么如何继续调整参数呢?这样就很难调整了。

损失函数的意义在于,能评估权重细微的改变对模型效果的影响。

损失函数应当是连续可导的。

4.3 数值微分

写一个求导的函数!

导数定义

据此可以写一段函数:

def numerical_diff(f,x):
    h = 10e-4 # 思考:h值过大或过小会有什么问题?
    return (f(x + h)- f(x)) / h

这段函数计算的是函数f在(x + h)和 x 之间的差分,这称为前向差分,我们还可以利用中心差分,即

def numerical_diff(f,x):
    h = 10e-4
    return (f(x + h)- f(x - h)) / 2 * h

这样求得的导数值更精确。

tip:以上利用微小的差分求导的方法被称为数值微分(numercical differentiation),而基于数字公式的求导过程,则用“解析性(analytic)”一词,利用解析性的求导是不含误差的。

4.4 梯度

梯度的定义:
对于函数, 的其所有自变量的偏导数组成的向量称为梯度。

代码实现:

import numpy as np

def numerical_gradient(f, x):
    h = 1e-4
    grad = np.zeros_like(x) # 生成一个和x形状相同、元素全为1 的数组

    for idx in range(x.size):
        tmp_val = x[idx]
        # f(x + h)的计算
        x[idx] = tmp_val + h
        fxh1 = f(x) #疑惑:应该是f(x[idx])还是f(x)呢?

        # f(x - h)的计算
        x[idx] = tmp_val - h
        fxh2 = f(x) #疑惑:应该是f(x[idx])还是f(x)呢?

        grad[idx] = (fxh1 - fxh2) / (2 * h)
        x[idx] = tmp_val # 还原数据
    return grad


#其实这段程序的代码我感觉怪怪的

实例:求的梯度并完成绘图。(随书源码)

# coding: utf-8
# cf.http://d.hatena.ne.jp/white_wheels/20100327/p3
import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D

def _numerical_gradient_no_batch(f, x):
    h = 1e-4  # 0.0001
    grad = np.zeros_like(x)

    for idx in range(x.size):
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x)  # f(x+h)

        x[idx] = tmp_val - h 
        fxh2 = f(x)  # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)

        x[idx] = tmp_val 

    return grad


def numerical_gradient(f, X):
    if X.ndim == 1:
        return _numerical_gradient_no_batch(f, X)
    else:
        grad = np.zeros_like(X)

        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_no_batch(f, x)

        return grad


def function_2(x):
    if x.ndim == 1:
        return np.sum(x**2)
    else:
        return np.sum(x**2, axis=1)


def tangent_line(f, x):
    d = numerical_gradient(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y


if __name__ == '__main__':
    x0 = np.arange(-2, 2.5, 0.25)
    x1 = np.arange(-2, 2.5, 0.25)
    X, Y = np.meshgrid(x0, x1)

    X = X.flatten()
    Y = Y.flatten()

    grad = numerical_gradient(function_2, np.array([X, Y]).T).T

    plt.figure()
    plt.quiver(X, Y, -grad[0], -grad[1],  angles="xy",color="#666666")
    plt.xlim([-2, 2])
    plt.ylim([-2, 2])
    plt.xlabel('x0')
    plt.ylabel('x1')
    plt.grid()
    plt.draw()
    plt.show()

得到的图像:

请务必牢记:梯度指向的方向是各点处函数值减少最多的方向

4.4.1 梯度法

神经网络学习的目的在于寻找最优参数,即损失函数取最小值时的参数。
利用梯度来寻找损失函数最小值的方法就是梯度法

注意: 函数值减少最多的方向 ≠ 函数值最小的方向,所以梯度方向未必是最小值的方向,梯度仅仅是寻找最小值的一个依据。

请辨析以下高数知识:

  • 极小值
  • 最小值
  • 鞍点(saddle point 国内一般称之为”驻点”)

梯度法求得的是极小值 神经网络的目标是最小值

在梯度法中,函数的取值从当前位置沿着梯度方向前进一定距离,然后在新的地方求梯度,再沿着梯度方向前进,如此反复,不断沿梯度方向前进。 像这样,通过不断沿梯度方向前进,逐渐减小函数值的过程就是梯度法(gradient method)

根据目的是寻找极大值还是极小值,梯度法的叫法也不同。寻找最小值的梯度法称为梯度下降法(gradient desent method) 寻找最小值的梯度法称为梯度上升法(gradient ascent method) 神经网络中的梯度法一般是梯度下降法。

我们用数学公式来表示梯度法:

式(4.7)

式(4.7)中的表示更新量,在神经网络的学习中,称为学习率(learning rate)。学习率决定在一次学习中,需要在多大程度上更新参数。

(个人理解:当偏导数为正数,该式能够使自变量的值减少,反正则会增加,直到自变量抵达极小值点)

学习率过大或过小都会影响学习效果,在神经网络的学习中,学习率被预先设定为某个值,在学习过程中,学习率也会随之改变

梯度下降法的python实现:

import numpy as np

def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x

    for i in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x

梯度法求的最小值

# coding: utf-8
import numpy as np
import matplotlib.pylab as plt
from gradient_2d import numerical_gradient


def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    x_history = []

    for i in range(step_num):
        x_history.append( x.copy() )

        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x, np.array(x_history)


def function_2(x):
    return x[0]**2 + x[1]**2

init_x = np.array([-3.0, 4.0])    

lr = 0.1
step_num = 20
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)

plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')

plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("X0")
plt.ylabel("X1")
plt.show()

得到的图像:

可以看到,自变量不断向(0,0)点逼近。

实际上,(x,y) = (0,0)就是该问题的解。
我们可以把神经网络的参数看成这里的x、y,就能逐步得出loss = 0的时候神经网络的参数取值了

像学习率这样的参数称为超参数(hyperparameter),神经网络中的权重参数等是在学习中自动获得的,而超参数则是人工设定的。一般来说,超参数需要不断尝试新的值

4.4.2 神经网络的梯度

神经网络的学习中,“梯度”一般指的是损失函数关于权重参数的梯度。
设权重为W, 损失函数为L:

求梯度代码的python实现(随书源码):

先定义一个simpleNet类

import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient
class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3) #用高斯分布进行初始化

    def predict(self, x): # 预测函数。返回值为神经网络的输出结果
        return np.dot(x, self.W)

    def loss(self, x, t): #损失函数
        z = self.predict(x)
        y = softmax(z) 
        loss = cross_entropy_error(y, t)

        return loss

下面是具体使用:


import sys, os
sys.path.append(os.pardir) 
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient


class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3)

    def predict(self, x):
        return np.dot(x, self.W)

    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)

        return loss

x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])

net = simpleNet()
p = net.predict(x)
print(p) # [-0.64406265  0.75226208  0.3971255 ]
print(np.argmax(p)) # 1
print(net.loss(x, t)) # 1.0222385512028003

上面的代码测试了随机生成的神经网络的预测值。可以看到误差非常大。
接下来求梯度:

f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)
print(dW)

4.5学习算法的实现

复习一下之前的内容:
前提:
何为 “学习”?。调整神经网络的权重和参数以便拟合训练数据的过程称为“学习”。

步骤1 (mini-batch)

从训练数据中随机选出一部分数据。我们将其视作全体数据

步骤2 (计算梯度)

计算梯度是为了更好地减少损失函数的值

步骤3 (更新参数)

将权重参数沿梯度方向进行微小更新。

步骤4(重复)

重复步骤1、2、3

该算法采用的数据来自于随机选择的mini-batch数据,因此也被称为随机梯度下降法

4.5.1 2层神经网络的实现

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 
from common.functions import *
from common.gradient import numerical_gradient
import numpy as np


class TwoLayerNet:

    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        # 初始化。
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)#返回值为一个用0填充的数组
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)
    # params保存着神经网络的参数。其中W1表示第一层的权重、权重为随机值,偏置为0,
    # hidden_size是隐藏层的神经元数,设置为一个合适的值即可
    #第0、1、2层分别是输入层、隐藏层、输出层


    def predict(self, x):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        return y
    # 预测函数,两层神经网络分别用sigmoid函数和softmax函数作激活函数



    def loss(self, x, t):
        y = self.predict(x)

        return cross_entropy_error(y, t)
    # x为输入数据,t为正确解标签 返回的是交叉熵误差


   def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)

        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
    #应该是评估精确度


   def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)   
        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
        return grads
        #计算梯度    


    def gradient(self, x, t):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}

        batch_num = x.shape[0]

        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)

        # backward
        dy = (y - t) / batch_num
        grads['W2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)

        dz1 = np.dot(dy, W2.T)
        da1 = sigmoid_grad(a1) * dz1
        grads['W1'] = np.dot(x.T, da1)
        grads['b1'] = np.sum(da1, axis=0)

        return grads
        # 误差反向传播法 高速计算梯度

每一个参数对应着一个梯度
如grads[‘b1’]和params[‘b1’]维度是相同的。

4.5.2 mini-batch的实现

神经网络的学习主要是使用mini-batch学习
mini-batch学习:从训练数据中选择一部分数据(称为mini-batch),再以这些mini-batch为输入数据,使用梯度法更新参数。

下面我们以TwoLayerNet为对象,进行mini-batch学习
以下为随书源码

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# 读入完整的MINIST数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

#初始化一个神经网络
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

iters_num = 10000  #循环次数
train_size = x_train.shape[0]
batch_size = 100 #批(batch)大小
learning_rate = 0.1 # 学习率

train_loss_list = []
#train_acc_list = []
#test_acc_list = []

#iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    # 计算梯度
    grad = network.gradient(x_batch, t_batch)

    # 更新参数
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]

    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)

    # if i % iter_per_epoch == 0:
    #     train_acc = network.accuracy(x_train, t_train)
    #     test_acc = network.accuracy(x_test, t_test)
    #     train_acc_list.append(train_acc)
    #     test_acc_list.append(test_acc)
    #     print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))

# 绘制图像
# markers = {'train': 'o', 'test': 's'}
# x = np.arange(len(train_acc_list))
# plt.plot(x, train_acc_list, label='train acc')
# plt.plot(x, test_acc_list, label='test acc', linestyle='--')
# plt.xlabel("epochs")
# plt.ylabel("accuracy")
# plt.ylim(0, 1.0)
# plt.legend(loc='lower right')
# plt.show()

注意被注释掉的代码,涉及到下一个问题:评价识别精度。

4.5.3 基于测试数据的评价

经过神经网络的学习,损失函数的值可以不断减小,但这个“减小”仅仅是针对某个特定mini-batch的,假如换成另外的数据,是不是也能减小呢?
这就涉及到神经网络的评价。需要利用训练用的mini-batch以外的数据来进行评价。

每经过一个epoch,就评估一次识别精度。

何为epoch? 即:完整地“看”过一次训练数据所用的次数。
比如训练数据大小为1000,mini-batch大小为100,经过10次就可以“看”完所有的训练数据
上面的代码中,每次更新参数后都会重新随机选择mini-batch,10次未必能看完所有数据。但是实际应用中,一般是将训练数据随机打乱。比如训练数据为1000,mini-batch大小为100,将1000个数据标为1、2、…1000,第一次训练选择1~100,第二次选择101~200…这样经过10次,就能看完所有数据

识别精度

可以看到,刚才的那段代码识别精度在稳步提升